<?php
/**
 * PHP Markdown Derivated - A text-to-HTML conversion tool
 * for web writers based on Markdown.
 * 
 * Copyright 2007 Julien Portalier.
 * <http://www.webcomics.fr/opensource/syntax>
 * 
 * Original PHP Markdown
 * Copyright (c) 2004-2007 Michel Fortin  
 * <http://www.michelf.com/projects/php-markdown/>
 * 
 * Original Markdown
 * Copyright (c) 2004-2006 John Gruber  
 * <http://daringfireball.net/projects/markdown/>
 * 
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 * 
 *   * Redistributions of source code must retain the above copyright notice,
 *     this list of conditions and the following disclaimer.
 * 
 *   * Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 * 
 *   * Neither the name "Markdown" nor the names of its contributors may
 *     be used to endorse or promote products derived from this software
 *     without specific prior written permission.
 * 
 * This software is provided by the copyright holders and contributors "as
 * is" and any express or implied warranties, including, but not limited
 * to, the implied warranties of merchantability and fitness for a
 * particular purpose are disclaimed. In no event shall the copyright owner
 * or contributors be liable for any direct, indirect, incidental, special,
 * exemplary, or consequential damages (including, but not limited to,
 * procurement of substitute goods or services; loss of use, data, or
 * profits; or business interruption) however caused and on any theory of
 * liability, whether in contract, strict liability, or tort (including
 * negligence or otherwise) arising in any way out of the use of this
 * software, even if advised of the possibility of such damage.
 */
class MarkdownSyntax
{
	const NoMarkup    = true;
	const AllowMarkup = false;
	
	// options
	public $noMarkup   = true;
#	public $noEntities = false;
	public $tabWidth   = 4;
	
	// reusable patterns to match lists
	public $marker_ul  = '[*+-]';
	public $marker_ol  = '\d+[.]|\#';
	
	public $escapeChars = '\`*_{}[]()>#+-.!';
	public $allowedTags = 'em|strong|cite|q|sup|sub|ins|del|hr|br|img|abbr|acronym|area';
	
	// generated content
	public $toc = array();
	
	// transformations for the document
	// ordered by descending priority
	protected $documentGamut = array(
		'runBlockGamut',
		'doFootnotes'
	);
	
	// transformations for block-level elements
	// ordered by descending priority
	protected $blockGamut = array(
		'doHeaders',
		'doHorizontalRules',
		'doLists',
		'doCodeBlocks',
		'doBlockQuotes',
		'formParagraphs'
	);
	
	// transformations for *inside* block-level elements
	// ordered by descending priority
	protected $spanGamut = array(
		'parseSpan',
		'parseMedias',
		'parseAnchors',
		'parseAutoLinks',
		'encodeAmpsAndAngles',
		'parseTextFormating',
		'parseHardBreaks',
		'parseFootnotes',
#		'parseSmiles',
		'smartyPants',
	);
	
	// internals
	protected $listLevel  = 0;
	protected $htmlHashes = array();
	protected $footnotes  = array();
	
	protected $htmlTagsBlockA = 'ins|del';
	protected $htmlTagsBlockB = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|address|script|noscript|form|fieldset|iframe|math|li';
	protected $htmlTagsMayContainBlocks = array('div', 'dl', 'blockquote', 'ul', 'ol', 'form', 'fieldset', 'iframe');
	
	protected $imageSizes = array('square', 'thumbnail', 'small', 'medium', 'large');
	
	
	/**
	 * Transform to XHTML. 
	 */
	function transform($text, $noMarkup=self::NoMarkup)
	{
		$this->noMarkup = $noMarkup;
		
		# Clear the global hashes.
		$this->html_hashes = array();
		$this->toc         = array();
		$this->footnotes   = array();
		$this->footnote_index = 1;
		
		# Standardizes line endings (DOS or Mac to *nix):
		$text = preg_replace('{\r\n?}', "\n", $text);
		
		# $text must ends with a couple of newlines:
		$text .= "\n\n";
		
		# Convert all tabs to spaces.
		$text = str_replace("\t", str_repeat(" ", $this->tabWidth), $text);
		
		# Turn block-level HTML blocks into hash entries
		$text = $this->hashHTMLBlocks($text);
		
		# Strip any lines consisting only of spaces and tabs.
		# This makes subsequent regexen easier to write, because we can
		# match consecutive blank lines with /\n+/ instead of something
		# contorted like /[ ]*\n+/ .
		$text = preg_replace('/^[ ]+$/m', '', $text);
		
		# Run document gamut methods
		foreach($this->documentGamut as $method)
			$text = $this->$method($text);
		
		# Clean and repair markup
		if (function_exists('tidy_repair_string'))
			$text = tidy_repair_string($text, APP.'config'.DS.'tidy.conf', 'utf8');
		
		return $text;
	}
	
	/**
	 * Generate a Table of Content. 
	 */
	function generateTableOfContent(array $toc=null)
	{
		if (is_null($toc))
			$toc =& $this->toc;
		if (empty($toc) or count($toc) < 3)
			return '';
		
		$str = '';
		foreach($toc as $i => $t)
		{
			list($lvl, $title, $id) = $t;
			$str .= str_repeat(" ", $lvl * 2 - 2).'* "'.$title.'":#h-'.$id."\n";
		}
		return '<ul class="toc">'.$this->unhash($this->_processListItems($str, $this->marker_ul))."</ul>\n\n";
	}
	
	// document-level functions
	
	/**
	 * Parse and protect HTML blocks.
	 */
	function hashHtmlBlocks($text)
	{
		if ($this->noMarkup)
			#return str_replace(array('<', '>'), array('&lt;', '&gt;'), $text);
			return $text;
		
		$less_than_tab = $this->tabWidth - 1;
		
		# Hashify HTML blocks:
		# We only want to do this for block-level HTML tags, such as headers,
		# lists, and tables. That's because we still want to wrap <p>s around
		# "paragraphs" that are wrapped in non-block-level tags, such as anchors,
		# phrase emphasis, and spans. The list of tags we're looking for is
		# hard-coded:
		#
		# *  List "a" is made of tags which can be both inline or block-level.
		#    These will be treated block-level when the start tag is alone on 
		#    its line, otherwise they're not matched here and will be taken as 
		#    inline later.
		# *  List "b" is made of tags which are always block-level;
		#
		
		# Regular expression for the content of a block tag.
		$nested_tags_level = 4;
		$attr = '
			(?>				# optional tag attributes
			  \s			# starts with whitespace
			  (?>
				[^>"/]+		# text outside quotes
			  |
				/+(?!>)		# slash not followed by ">"
			  |
				"[^"]*"		# text inside double quotes (tolerate ">")
			  |
				\'[^\']*\'	# text inside single quotes (tolerate ">")
			  )*
			)?	
			';
		$content =
			str_repeat('
				((?>
				  [^<]+			# content without tag
				|
				  <\2			# nested opening tag
					'.$attr.'	# attributes
					(?>
					  />
					|
					  >', $nested_tags_level).	# end of opening tag
					  '.*?'.					# last level nested tag content
			str_repeat('
					  </\2\s*>	# closing nested tag
					)
				  |				
					<(?!/\2\s*>	# other tags with a different name
				  )
				)*)',
				$nested_tags_level);
		$content2 = str_replace('\2', '\3', $content);
		
		# First, look for nested blocks, e.g.:
		# 	<div>
		# 		<div>
		# 		tags for inner block must be indented.
		# 		</div>
		# 	</div>
		#
		# The outermost tags must start at the left margin for this to match, and
		# the inner nested divs must be indented.
		# We need to do this before the next, more liberal match, because the next
		# match will start at the first `<div>` and stop at the first `</div>`.
		$text = preg_replace_callback('{(?>
			(?>
				(?<=\n\n)		# Starting after a blank line
				|				# or
				\A\n?			# the beginning of the doc
			)
			(						# save in $1

			  # Match from `\n<tag>` to `</tag>\n`, handling nested tags 
			  # in between.
					
						[ ]{0,'.$less_than_tab.'}
						<('.$this->htmlTagsBlockB.')# start tag = $2
						'.$attr.'>			# attributes followed by > and \n
						'.$content.'		# content, support nesting
						</\2>				# the matching end tag
						[ ]*				# trailing spaces/tabs
						(?=\n+|\Z)	# followed by a newline or end of document

			| # Special version for tags of group a.

						[ ]{0,'.$less_than_tab.'}
						<('.$this->htmlTagsBlockA.')# start tag = $3
						'.$attr.'>[ ]*\n	# attributes followed by >
						'.$content2.'		# content, support nesting
						</\3>				# the matching end tag
						[ ]*				# trailing spaces/tabs
						(?=\n+|\Z)	# followed by a newline or end of document
					
			| # Special case just for <hr />. It was easier to make a special 
			  # case than to make the other regex more complicated.
			
						[ ]{0,'.$less_than_tab.'}
						<(hr)				# start tag = $2
						\b					# word break
						([^<>])*?			# 
						/?>					# the matching end tag
						[ ]*
						(?=\n{2,}|\Z)		# followed by a blank line or end of document
			
			| # Special case for standalone HTML comments:
			
					[ ]{0,'.$less_than_tab.'}
					(?s:
						<!-- .*? -->
					)
					[ ]*
					(?=\n{2,}|\Z)		# followed by a blank line or end of document
			
			| # PHP and ASP-style processor instructions (<? and <%)
			
					[ ]{0,'.$less_than_tab.'}
					(?s:
						<([?%])			# $2
						.*?
						\2>
					)
					[ ]*
					(?=\n{2,}|\Z)		# followed by a blank line or end of document
					
			)
			)}Sxmi',
			array($this, '_hashHTMLBlocks_callback'), $text);
		
		return $text;
	}
	
	function _hashHTMLBlocks_callback($match)
	{
		if (!isset($match[2], $match[3]))
			return $this->hashBlock($match[1]);
		
		$text = $match[1];
		$tag  = $match[2];
		$orig_subtext = $match[3];
		$subtext      = $orig_subtext;
		
		if (!isset($this->htmlTagsBlocks))
		{
			$this->htmlTagsBlocks = array_merge(
				explode('|', $this->htmlTagsBlockA),
				explode('|', $this->htmlTagsBlockB)
			);
		}
		
		if (in_array($tag, $this->htmlTagsMayContainBlocks))
		{
			# recursive hash
			$subtext = $this->outdent($subtext);
			$subtext = $this->hashHTMLBlocks($subtext);
			$subtext = $this->runBlockGamut($subtext);
			
			# replaces (in text) original subtext with parsed one.
			$text = str_replace($orig_subtext, "\n$subtext\n", $text);
			
			$key = $this->hashBlock($text);
			return "\n\n$key\n\n";
		}
		elseif (in_array($tag, $this->htmlTagsBlocks))
		{
			# hashes block
			$subtext = $this->outdent($subtext);
			$subtext = $this->runSpanGamut($subtext);
			
			# replaces (in text) original subtext with parsed one.
			$text = str_replace($orig_subtext, $subtext, $text);
			
			$key = $this->hashBlock($text);
			return "\n\n$key\n\n";
		}
		else
		{
			# transforms inline
			$subtext = $this->runSpanGamut($subtext);
			
			# replaces (in text) original subtext with parsed one.
			$text = str_replace($orig_subtext, $subtext, $text);
			
			return $this->hashPart($text);
		}
	}
	
	/**
	 * Block-level transformations.
	 */
	function runBlockGamut($text)
	{
		foreach ($this->blockGamut as $method)
			$text = $this->$method($text);
		return $text;
	}
	
	/**
	 * Inside block-level transformations.
	 */
	function runSpanGamut($text)
	{
		foreach ($this->spanGamut as $method)
			$text = $this->$method($text);
		return $text;
	}
	
	
	// Block-level transformations
	
	function formParagraphs($text)
	{
		# Strip leading and trailing lines
		$text  = preg_replace('/\A\n+|\n+\z/', '', $text);
		
		# Parse each block
		$blocks = preg_split('/\n{2,}/', $text, -1, PREG_SPLIT_NO_EMPTY);
		
		foreach ($blocks as $key => $block)
		{
			if (!preg_match('/^B\x1A[0-9]+B$/', $block))
			{
				# Is a paragraph.
				$block  = $this->runSpanGamut($block);
				$block  = preg_replace('/^([ ]*)/', "<p>", $block);
				$block .= "</p>";
				$block  = $this->unhash($block);
			}
			else
			{
				# Another kind (already parsed).
				$block = $this->htmlHashes[$block];
			}
			
			$blocks[$key] = $block;
		}
		return implode("\n\n", $blocks);
	}
	
	
	/**
	 * Adds previously parsed footnotes at the end of the document.
	 */
	function doFootnotes($text)
	{
		if (!empty($this->footnotes))
		{
#			$text .= "\n<ul class=\"footnotes\">";
#			foreach($this->footnotes as $fn)
#				$text .= '<li><a href="#ref-'.$fn[0].'" id="fn-'.$fn[0].'">'.$fn[0].'.</a> '.$fn[1].'</li>';
#			$text .= "\n</ul>\n\n";
			$text .= "\n<ol class=\"footnotes\">";
			foreach($this->footnotes as $fn)
				$text .= "<li>{$fn[1]}</li>";
			$text .= "\n</ol>\n\n";
		}
		return $text;
	}
	
	/**
	 * Transforms footnotes.
	 * 
	 * This is a paragraph((And this is a footnote.)), just
	 * a simple paragraph, but it could be a list((Yep.)), a blockquote,
	 * or anything else((Yep again)).
	 */
	function parseFootnotes($text)
	{
		return preg_replace_callback('{ \(\( (.+?) \)\) }xs',
			array($this, '_parseFootnotes_callback'), $text);
	}
	
	protected function _parseFootnotes_callback($match)
	{
#		static $i = 0;
#		$i++;
		$i = $this->footnote_index++;
		
		$note  = $this->unhash($match[1]);
		$title = strip_tags($note);
		$this->footnotes[] = array($i, $note);
		
#		return $this->hashPart('<sup class="footnote"><a href="#fn-'.$i.'" id="ref-'.$i.'" title="'.$title.'">'.$i.'</a></sup>');
		return $this->hashPart('<sup class="footnote" title="'.htmlspecialchars($title).'">'.$i.'</sup>');
	}
	
	/**
	 * Transforms headers.
	 * 
	 * == Header lvl 2
	 * === header lvl 3 with optional closing ===
	 * =header lvl 1
	 */
	function doHeaders($text)
	{
		return preg_replace_callback('{^(={1,6})[ ]*(.+?)[ ]*=*\n+}xm',
			array($this, '_doHeaders_callback'), $text);
	}
	
	protected function _doHeaders_callback(array $match)
	{
		$lvl = strlen($match[1]);
		$h   = $this->runSpanGamut($match[2]);
		$id  = Inflector::slug($match[2]);
		$this->toc[] = array($lvl, $h, $id);
		return $this->hashBlock("<h{$lvl} id=\"h-$id\">{$match[2]}</h{$lvl}>")."\n\n";
	}
	
	/**
	 * Transforms horizontal rules.
	 */
	function doHorizontalRules($text)
	{
		return preg_replace_callback('{^-{4,}\n+}xm', array($this, '_doHorizontalRules_callback'), $text);
	}
	
	protected function _doHorizontalRules_callback(array $match)
	{
		return $this->hashBlock("<hr />")."\n\n";
	}
	
	/**
	 * Transforms (un)ordered lists.
	 * 
	 *   1. item
	 *   2. item
	 *     * sub-item
	 *   3. item
	 *     - sub-item
	 *   4. item
	 *     + sub-item one
	 *     + sub-item two
	 * 
	 *   # another ordered list
	 *   # second item
	 */
	function doLists($text)
	{
		$less_than_tab = $this->tabWidth - 1;
		
		# for each kind a list: UL + OL
		foreach (array($this->marker_ul, $this->marker_ol) as $marker)
		{
			# re-usable pattern the match entire list:
			$whole_list = '
				(								# $1 = whole list
				  (								# $2
					[ ]{0,'.$less_than_tab.'}
					('.$marker.')				# $3 = first list item marker
					[ ]+
				  )
				  (?s:.+?)
				  (								# $4
					  \z
					|
					  \n{2,}
					  (?=\S)
					  (?!						# Negative lookahead for another list item marker
						[ ]*
						'.$marker.'[ ]+
					  )
				  )
				)
			';
			
			# we use a different prefix before nested lists than top-level lists.
			if ($this->listLevel)
			{
				$text = preg_replace_callback('{^'.$whole_list.'}mx',
					array($this, '_doLists_callback'), $text);
			}
			else
			{
				$text = preg_replace_callback('{
						(?:(?<=\n)\n|\A\n?) # Must eat the newline
						'.$whole_list.'
					}mx',
					array($this, '_doLists_callback'), $text);
			}
		}
		
		return $text;
	}
	
	protected function _doLists_callback(array $match)
	{
		if (preg_match("/{$this->marker_ul}/", $match[3]))
		{
			$type   = 'ul';
			$marker = $this->marker_ul;
		}
		else
		{
			$type   = 'ol';
			$marker = $this->marker_ol;
		}
		
		$list = $match[1]."\n";
		$list = $this->_processListItems($list, $marker);
		
		return "\n".$this->hashBlock("<$type>\n$list</$type>")."\n\n";
	}
	
	/**
	 * Process the contents of a single ordered or unordered list, splitting it
	 * into individual list items.
	 */
	protected function _processListItems($str, $marker)
	{
		# The $this->listLevel global keeps track of when we're inside a list.
		# Each time we enter a list, we increment it; when we leave a list,
		# we decrement. If it's zero, we're not in a list anymore.
		# 
		# We do this because when we're not inside a list, we want to treat
		# something like this:
		#
		#		I recommend upgrading to version
		#		8. Oops, now this line is treated
		#		as a sub-list.
		# 
		# As a single paragraph, despite the fact that the second line starts
		# with a digit-period-space sequence.
		# 
		# Whereas when we're inside a list (or sub-list), that line will be
		# treated as the start of a sub-list. What a kludge, huh? This is
		# an aspect of Markdown's syntax that's hard to parse perfectly
		# without resorting to mind-reading. Perhaps the solution is to
		# change the syntax rules such that sub-lists must start with a
		# starting cardinal number; e.g. "1." or "a.".
		$this->listLevel++;
		
		# trim trailing blank lines:
		$str = preg_replace("/\n{2,}\\z/", "\n", $str);
		
		$str = preg_replace_callback('{
				(\n)?                       # leading line       = $1
				(^[ ]*)                     # leading whitespace = $2
				('.$marker.') [ ]+          # list marker        = $3
				((?s:.+?))                  # list item text     = $4
				(?:(\n+(?=\n))|\n)          # tailing blank line = $5
				(?= \n* (\z | \2 ('.$marker.') [ ]+))
				}xm',
			array($this, '_processListItems_callback'),
			$str);
		
		$this->listLevel--;
		return $str;
	}
	
	protected function _processListItems_callback(array $match)
	{
		$item               =  $match[4];
		$leading_line       =& $match[1];
		$leading_space      =& $match[2];
		$tailing_blank_line =& $match[5];

		if (!empty($leading_line) or !empty($tailing_blank_line) or preg_match('/\n{2,}/', $item))
			$item = $this->runBlockGamut($this->outdent($item)."\n");
		else
		{
			# Recursion for sub-lists:
			$item = $this->doLists($this->outdent($item));
			$item = preg_replace('/\n+$/', '', $item);
			$item = $this->runSpanGamut($item);
		}
		
		return "<li>$item</li>\n";
	}
	
	/**
	 * Transforms preformated code.
	 * 
	 *     <?php
	 *     $toto = 'brocoli';
	 *     ?>
	 */
	function doCodeBlocks($text)
	{
		return preg_replace_callback('{
				(?:\n\n|\A)
				(                                       # $1 = the code block -- one or more lines, starting with a space/tab
				  (?>
					[ ]{'.$this->tabWidth.'}           # Lines must start with a tab or a tab-width of spaces
					.*\n+
				  )+
				)
				((?=^[ ]{0,'.$this->tabWidth.'}\S)|\Z)	# Lookahead for non-space at line-start, or end of doc
			}xm',
			array($this, '_doCodeBlocks_callback'), $text);
	}
	
	protected function _doCodeBlocks_callback(array $match)
	{
		$cb = $this->outdent($match[1]);
		
		# Protects HTML special chars. If noMarkup is set, then
		# '<' and '>' have already been transformed, so we
		# won't transform them again.
#		$cb = (!$this->noMarkup) ?
#			htmlspecialchars($cb, ENT_NOQUOTES) :
#			preg_replace('{&(?![gl]t;)}', '&amp;', $cb);
		$cb = htmlspecialchars($cb, ENT_NOQUOTES);
		
		# trim leading newlines and trailing newlines
		$cb = preg_replace('/\A\n+|\n+\z/', '', $cb);
		
		$cb = "<pre><code>$cb\n</code></pre>";
		return "\n\n".$this->hashBlock($cb)."\n\n";
	}
	
	/**
	 * Transforms BlockQuotes.
	 * 
	 * > This is a
	 * > blockquoted text.
	 */
	function doBlockQuotes($text)
	{
		# wraps all match in $1
		# ">" at start of a line
		# + rest of the line
		# + any subsequent consecutive lines
		# ends with a blank line
		return preg_replace_callback('{((?>^[ ]*(>|\&gt\;)[ ]?.+\n(.+\n)*\n*)+)}xm',
			array($this, '_doBlockQuotes_callback'), $text);
	}
	
	protected function _doBlockQuotes_callback(array $match)
	{
		# trim one level of quoting - trim whitespace-only lines
		$bq = preg_replace('/^[ ]*(>|&gt;)[ ]?|^[ ]+$/m', '', $match[1]);
		
		# parses blockquote for blocks
		$bq = $this->runBlockGamut($bq);
		
		return $this->hashBlock("<blockquote>$bq</blockquote>")."\n\n";
	}
	
	
	// Inside block-level transformations
	
	/**
	 * Take the string $str and hash embeded HTML, escaped
	 * characters and handle code spans.
	 */
	function parseSpan($str)
	{
		$tags = $this->noMarkup ? $this->allowedTags : '\w+';
		
		$attr = '
		(?>				# optional tag attributes
		  \s			# starts with whitespace
		  (?>
			[^>"/]+		# text outside quotes
		  |
			/+(?!>)		# slash not followed by ">"
		  |
			"[^"]*"		# text inside double quotes (tolerate ">")
		  |
			\'[^\']*\'	# text inside single quotes (tolerate ">")
		  )*
		)?	
		';
		
		# hash inline tags
		$regexp = '{<(('.$tags.')'.$attr.')>(.+?)</(?>\2)>}xs';
		$str = preg_replace_callback($regexp, array($this, '_parseSpan_hash_inline_tags'), $str);
		
		# hash empty tags
		$regexp = '{<(('.$tags.')'.$attr.')[ ]?/?>}xs';
		$str = preg_replace_callback($regexp, array($this, '_parseSpan_hash_empty_tags'), $str);
		
		# encode entities
#		$str = htmlspecialchars($str, ENT_NOQUOTES);
		
		# escaped characters
		$str = preg_replace_callback('{(\\\\['.preg_quote($this->escapeChars).'])}',
			array($this, '_parseSpan_escape_char_callback'), $str);
		
		# inline code spans
		$str = preg_replace_callback('{(?<![`\\\\])`(.+?)`}',
			array($this, '_parseSpan_codespan_callback'), $str);
		
		return $str;
	}
	
	protected function _parseSpan_hash_inline_tags($match)
	{
		$match[3] = $this->runSpanGamut($match[3]);
#		$match[3] = htmlspecialchars($match[3], ENT_NOQUOTES);
		return $this->hashPart("<{$match[1]}>{$match[3]}</{$match[2]}>");
	}
	
	protected function _parseSpan_hash_empty_tags($match)
	{
		return $this->hashPart("<{$match[1]} />");
	}
	
	protected function _parseSpan_escape_char_callback($match)
	{
		return $this->hashPart("&#". ord($match[0]). ";");
	}
	
	protected function _parseSpan_codespan_callback($match)
	{
		$code = htmlspecialchars(trim($match[1]), ENT_NOQUOTES);
		return $this->hashPart("<code>$code</code>");
	}
	
	
	/**
	 * Transforms links and clickable images.
	 * 
	 * "text(optional_title)":url
	 * "{{...}}($optional_title)":$url
	 */
	function parseAnchors($text)
	{
		# Clickable images
		# "{{...}}($optional_title)":$url
		$text = preg_replace_callback('#"({{.+?}})[ ]*(?(?=[(])[(]([^)"]+)[)])":([^<>\s\n]*)\b\/?#',
			array($this, '_parseAnchors_images_callback'), $text);
		
		# Links
		# "$text($optional_title)":$url
		$text = preg_replace_callback('#"([^"(]+)[ ]*(?(?=[(])[(]([^)"]+)[)])":([^<>\s\n]*)\b\/?#',
			array($this, '_parseAnchors_callback'), $text);
		
		return $text;
	}
	
	protected function _parseAnchors_callback($match)
	{
#		debug($match);
		
		$text  = $this->runSpanGamut($match[1]);
		$url   = $this->encodeAmpsAndAngles($match[3]);
		$title = empty($match[2]) ? '' : ' title="'.$match[2].'"';
		return $this->hashPart('<a href="'.$url.'"'.$title.'>'.$text.'</a>');
	}
	
	protected function _parseAnchors_images_callback($match)
	{
		$url   = $this->encodeAmpsAndAngles($match[3]);
		$title = empty($match[2]) ? '' : ' title="'.$match[2].'"';
		return $this->hashPart('<a href="'.$url.'"'.$title.'>'.$match[1].'</a>');
	}
	
	/**
	 * Transforms media files (images, sound, videos).
	 */
	function parseMedias($text)
	{
		return preg_replace_callback('/{{(.+?)}}/',
			array('self', '_parseMedias_callback'), $text);
	}
	
	protected function _parseMedias_callback($match)
	{
		$options  = explode('|', $match[1]);
		$url      = array_shift($options);
		$ext      = Files::getExtension($url);
		
		# options
		
		$class    = '';
		$alt      = null;
		$title    = '';
		$size     = '';
		$str_size = '';
		$autoplay = false;
		
		foreach($options as $opt)
		{
			if (in_array($opt, array('left', 'right', 'border')))
				$class .= "$opt ";
			elseif ($opt == 'autoplay')
				$autoplay = true;
			elseif (preg_match('/^[0-9]+x[0-9]+$/', $opt))
				$size = explode('x', $opt);
			elseif (in_array($opt, $this->imageSizes))
			{
				if (!isset($match[2]))
					$link = $url;
				$size = $opt;
			}
			elseif (!isset($alt))
				$alt = $opt;
			else
				$title = $opt;
		}
		
		# prepare some HTML
		
		if (is_array($size))
			$str_size = ' width="'.$size[0].'" height="'.$size[1].'"';
		
		if (!empty($class))
			$class = ' class="'.$class.'"';
		
		$url = $this->encodeAmpsAndAngles($url);
		
		switch(strtolower($ext))
		{
			# vector image
			case 'svg': case 'svgz':
				$media =
					'<object type="image/svg+xml" data="'.$url.'"'.$title.$str_size.$class.'>' .
						'<embed src="'.$url.'" type="image/svg+xml"'.$str_size.' />' .
					'</object>';
				break;
			
			# flash object
			case 'swf':
				$media =
					'<object type="application/x-shockwave-flash" pluginspage="http://www.macromedia.com/go/getflashplayer" data="'.$url.'"'.$title.$str_size.$class.'>'.
						'<param name="wmode" value="transparent" />'.
						'<param name="movie" value="'.$url.'" />'.
					'</object>';
				break;
			
			# podcast
			case 'mp3':
				$str_size = empty($str_size) ? 'width="200" height="20"' : $str_size;
				$media = 
					'<object type="application/x-shockwave-flash" data="/swf/neolao/player_mp3_maxi.swf" '.$str_size.'>
					    <param name="movie" value="/swf/neolao/player_mp3_maxi.swf" />
					    <param name="bgcolor" value="#ffffff" />
					    <param name="FlashVars" value="mp3='.$url.'&amp;autoplay='.($autoplay ? '1' : '0').'&amp;showstop=1&amp;showvolume=1&amp;showloading=always&amp;loadingcolor=ffffcc&amp;bgcolor1=f4f4f4&amp;bgcolor2=cccccc&amp;sliderovercolor=333333&amp;buttoncolor=333333&amp;buttonovercolor=ffffcc&amp;textcolor=333333" />
					</object>';
				break;
			
			# vidcast
			case 'flv':
				$str_size = empty($str_size) ? 'width="320" height="240"' : $str_size;
				$media =
					'<object type="application/x-shockwave-flash" data="/swf/neolao/player_flv_maxi.swf"'.$title.$str_size.$class.'>'.
						'<param name="wmode" value="transparent" />'.
						'<param name="movie" value="/swf/neolao/player_flv_maxi.swf" />'.
						'<param name="FlashVars" value="flv='.$url.'&amp;showtime=1&amp;margin=0&amp;playercolor=ffffff&amp;bgcolor1=ffffff&bgcolor2=ffffff&amp;buttoncolor=333333&amp;buttonovercolor=999999&amp;slidercolor1=333333&amp;slidercolor2=000000&amp;sliderovercolor=999999&amp;loadingcolor=0" />'.
					'</object>';
				break;
			
			# image
			default:
				$title = empty($title) ? '' : ' title="'.$title.'"';
				if (!empty($size))
				{
					if (is_array($size))
						$size = "{$size[0]}x{$size[1]}";
					$url = "/img.php?url=$url&amp;size=$size"; 
				}
				$media = "<img src=\"$url\" alt=\"$alt\" $title$class$str_size />";
		}
		
		return $this->hashPart(isset($link) ? "<a href=\"$link\">$media</a>" : $media);
	}
	
	/**
	 * Transforms inline links.
	 *
	 * http://toto.com/~brocoli
	 * brocoli@toto.com
	 */
	function parseAutoLinks($text)
	{
		# URL
		$text = preg_replace_callback('{(?!<.*?)(https?|ftps?|dict)://([^<>\s\n]*)\b\/?(?![^<>]*?>)}',
			array($this, '_parseAutoLinks_url_callback'), $text);
		
		# email addresses
		$text = preg_replace_callback('{
			(?:mailto:)?
			(
				[-.\w\x80-\xFF]+
				\@
				[-a-z0-9\x80-\xFF]+(\.[-a-z0-9\x80-\xFF]+)*\.[a-z]+
			)
			}xi',
			array($this, '_parseAutoLinks_email_callback'), $text);
		
		return $text;
	}
	
	protected function _parseAutoLinks_url_callback($match)
	{
		$url = $this->encodeAmpsAndAngles($match[0]);
		return $this->hashPart("<a href=\"$url\">$url</a>");
	}
	
	protected function _parseAutoLinks_email_callback($match)
	{
		$link = $this->encodeEmailAddress($match[1]);
		return $this->hashPart($link);
	}
	
	/**
	 * Input: an email address, e.g. "foo@example.com"
	 * 
	 * Output: the email address as a mailto link, with each character
	 * of the address encoded as either a decimal or hex entity, in
	 * the hopes of foiling most address harvesting spam bots. E.g.:
	 * 
	 * <p><a href="&#109;&#x61;&#105;&#x6c;&#116;&#x6f;&#58;&#x66;o&#111;
	 * &#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;&#101;&#46;&#x63;&#111;
	 * &#x6d;">&#x66;o&#111;&#x40;&#101;&#x78;&#97;&#x6d;&#112;&#x6c;
	 * &#101;&#46;&#x63;&#111;&#x6d;</a></p>
	 * 
	 * Based by a filter by Matthew Wickline, posted to BBEdit-Talk.
	 * With some optimizations by Milian Wolff.
	 */
	function encodeEmailAddress($addr)
	{
		$addr  = "mailto:$addr";
		$chars = preg_split('/(?<!^)(?!$)/', $addr);
		
		# Deterministic seed.
		$seed = (int)abs(crc32($addr) / strlen($addr));
		
		foreach ($chars as $key => $char)
		{
			$ord = ord($char);
			
			# Ignore non-ascii chars.
			if ($ord < 128)
			{
				 # Pseudo-random function.
				$r = ($seed * (1 + $key)) % 100;
				
				# roughly 10% raw, 45% hex, 45% dec
				# '@' *must* be encoded. I insist.
				if ($r > 90 && $char != '@') /* do nothing */;
				else if ($r < 45) $chars[$key] = '&#x'.dechex($ord).';';
				else              $chars[$key] = '&#'.$ord.';';
			}
		}
		
		$addr = implode('', $chars);
		
		# text without `mailto:`
		$text = implode('', array_slice($chars, 7));
		return "<a href=\"$addr\">$text</a>";
	}
	
	/**
	 * Transforms strong, emphasis and underline.
	 * 
	 * **strong**, //emphasis//, __underline__
	 */
	function parseTextFormating($text)
	{
		$chars = array('\*', '/', '_');
		foreach($chars as $char)
		{
			# marker:
			#   1. not preceded nor followed by marker;
			#   2. no space after start nor before end.
			$regexp ='{( (?<!'.$char.$char.') '.$char.$char.') (?=\S) (?! \1) (.+?) (?<=\S)(?<!\s(?=\1).) \1 }sx';
			$text = preg_replace_callback($regexp,
				array($this, '_parseTextFormating_callback'), $text);
		}
		return $text;
	}
	
	protected function _parseTextFormating_callback($match)
	{
		switch($match[1])
		{
			case '**': return "<strong>{$match[2]}</strong>";
			case '//': return "<em>{$match[2]}</em>";
			case '__': return "<u>{$match[2]}</u>";
			default:   return $match[2];
		}
	}
	
	/**
	 * Transforms forced linebreaks.
	 */
	function parseHardBreaks($text)
	{
		return str_replace("%%%\n", '<br /> ', $text);
	}
	
	# Smart processing for ampersands and angle brackets that need to be encoded.
	function encodeAmpsAndAngles($text)
	{
#		if ($this->noEntities)
#			return str_replace(array('&', '<', '>'), array('&amp;', '&lt;', '&gt;'), $text);
		
		# Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
		#   http://bumppo.net/projects/amputator/
		$text = preg_replace('/&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)/', '&amp;', $text);;
		
		# Encode naked <'s
#		$text = preg_replace('{<(?![a-z/?\$!%])}i', '&lt;', $text);
		
		# Encode naked >'s
#		$text = preg_replace('{(?<![a-z/?\$!%])>}i', '&gt;', $text);
		
		return $text;
	}
	
	/**
	 * Transform some entities, handle curly quotes and non-breaking spaces.
	 */
	function smartyPants($str)
	{
		# curly-quotes
		$str = preg_replace(
			array(
				'/(?!<.*?)"([^"<>]+?)"(?![^<>]*?>)/',
				"/``(.+?)''/",
			), '“\1”', $str);
		
		# some entities
		$str = str_replace(
			array(
				# arrows (<->, ->, <-, <=>, =>, <=>)
				'&lt;-&gt;', '-&gt;', '&lt;-',
				'&lt;=&gt;', '=&gt;', '&lt;=',
				# french quotes (<< >>)
				'&lt;&lt;', '&gt;&gt;',
				# dashes (em, en)
				'---', '--',
				#
				'...', '(c)', '(tm)', '(r)'
			),
			array(
				// arrows
				'&harr;', '&rarr;', '&larr;',
				'&hArr;', '&rArr;', '&lArr;',
				// french quotes
				'«', '»',
				// dashes
				'&mdash;', '&ndash;',
				//
				'&#8230;',
				'&copy;', '&trade;', '&reg;'
			), $str);
		
		# spaces (nbsp)
		$str = preg_replace(
			array(
				'/(\s+)(:|;|!|\?)/',
				'/(¿|¡)(\s+)/',
				'/«\s*(.+?)\s*»/',
				'/(\d)\s+(\d)/',
			),
			array(
				'&nbsp;\2',
				'\1&nbsp;',
				'&laquo;&nbsp;\1&nbsp;&raquo;',
				'\1&nbsp;\2',
			), $str);
		
		return $str;
	}
	
	
	/**
	 * Transform Smileys.
	 */
	function parseSmiles($text)
	{
		return $text;
	}
	
	
	// internals
	
	protected function hashBlock($text)
	{
		return $this->hashPart($text, 'B');
	}
	
	/**
	 * Called whenever a tag must be hashed when a function insert an atomic 
	 * element in the text stream. Passing $text to through this function gives
	 * a unique text-token which will be reverted back when calling unhash.
	 * 
	 * The $boundary argument specify what character should be used to surround
	 * the token. By convension, "B" is used for block elements that needs not
	 * to be wrapped into paragraph tags at the end, ":" is used for elements
	 * that are word separators and "S" is used for general span-level elements.
	 */
	protected function hashPart($text, $boundary='X')
	{
		static $i = 0;
		
		# Swap back any tag hash found in $text so we do not have to `unhash`
		# multiple times at the end.
		$text = $this->unhash($text);
		
		# Then hash the block.
		$key = "$boundary\x1A" . ++$i . $boundary;
		$this->htmlHashes[$key] = $text;
		
		 # String that will replace the tag.
		return $key;
	}
	
	protected function unHash($text)
	{
		return preg_replace_callback('/(.)\x1A[0-9]+\1/', 
			array($this, '_unhash_callback'), $text);
	}
	
	protected function _unhash_callback(array $match)
	{
		return $this->htmlHashes[$match[0]];
	}
	
	/**
	 * Remove one level of line-leading tabs or spaces.
	 */
	protected function outdent($text)
	{
		return preg_replace('/^(\t|[ ]{1,'.$this->tabWidth.'})/m', '', $text);
	}
}

function wc_markdown($text, $noMarkup=MarkdownSyntax::NoMarkup)
{
  $markdown = new MarkdownSyntax();
  return $markdown->transform($text, $noMarkup);
}

?>
